Skip to content

Calculate file options for WheelWriter once and cache the result#2865

Merged
messense merged 1 commit intoPyO3:mainfrom
e-nomem:wheel-file-options
Nov 21, 2025
Merged

Calculate file options for WheelWriter once and cache the result#2865
messense merged 1 commit intoPyO3:mainfrom
e-nomem:wheel-file-options

Conversation

@e-nomem
Copy link
Copy Markdown
Contributor

@e-nomem e-nomem commented Nov 20, 2025

We can store the file options directly after calculating the compression options and mtime once since they will never change.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR refactors the WheelWriter to cache file options (compression settings and modification time) at initialization rather than recalculating them for each file operation. The mtime() method is removed from WheelWriter and replaced with a new zip_mtime() function in BuildContext that is called once when creating the writer.

  • Moved SOURCE_DATE_EPOCH handling from WheelWriter::mtime() to BuildContext::zip_mtime()
  • Changed WheelWriter to store SimpleFileOptions instead of CompressionOptions
  • Updated lifetime annotation in CompressionOptions::get_file_options() from '_ to 'static

Reviewed Changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
src/module_writer/wheel_writer.rs Removed mtime() method; changed field from compression: CompressionOptions to file_options: SimpleFileOptions; simplified add_bytes() and finish() to use cached options; updated test to call get_file_options()
src/compression.rs Changed get_file_options() return type lifetime from '_ to 'static to support caching
src/build_context.rs Added zip_mtime() function; updated all 5 WheelWriter::new() call sites to compute and pass file_options instead of compression

Comment thread src/build_context.rs
Comment on lines +1422 to +1433
fn zip_mtime() -> DateTime {
let res = env::var("SOURCE_DATE_EPOCH")
.context("") // Only using context() to unify the error types
.and_then(|epoch| {
let epoch: i64 = epoch.parse()?;
let dt = time::OffsetDateTime::from_unix_timestamp(epoch)?;
let dt = DateTime::try_from(dt)?;
Ok(dt)
});

res.unwrap_or_default()
}
Copy link

Copilot AI Nov 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The old mtime() method in wheel_writer.rs enforced a minimum date of 1980-01-01 (the earliest timestamp a zip file can represent), but this logic is missing in the new zip_mtime() function. This could cause issues if SOURCE_DATE_EPOCH is set to a date before 1980-01-01.

The old implementation included:

let min_dt = time::Date::from_calendar_date(1980, time::Month::January, 1)
    .unwrap()
    .midnight()
    .assume_offset(time::UtcOffset::UTC);
let dt = dt.max(min_dt);

Consider adding this validation to zip_mtime():

fn zip_mtime() -> DateTime {
    let res = env::var("SOURCE_DATE_EPOCH")
        .context("")
        .and_then(|epoch| {
            let epoch: i64 = epoch.parse()?;
            let dt = time::OffsetDateTime::from_unix_timestamp(epoch)?;
            // Ensure the date is at least 1980-01-01 (earliest zip timestamp)
            let min_dt = time::Date::from_calendar_date(1980, time::Month::January, 1)
                .unwrap()
                .midnight()
                .assume_offset(time::UtcOffset::UTC);
            let dt = dt.max(min_dt);
            let dt = DateTime::try_from(dt)?;
            Ok(dt)
        });
    
    res.unwrap_or_default()
}

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

zip::DateTime::try_from() will return an error if the timestamp is before Jan 1 1980. Also zip::DateTime::default() returns a datetime of Jan 1 1980 so, together with the unwrap_or_default(), this code can never return a timestamp earlier than Jan 1 1980.

Comment thread src/build_context.rs
Comment on lines +1423 to +1432
let res = env::var("SOURCE_DATE_EPOCH")
.context("") // Only using context() to unify the error types
.and_then(|epoch| {
let epoch: i64 = epoch.parse()?;
let dt = time::OffsetDateTime::from_unix_timestamp(epoch)?;
let dt = DateTime::try_from(dt)?;
Ok(dt)
});

res.unwrap_or_default()
Copy link

Copilot AI Nov 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The empty context string context("") is unusual and appears to be used solely to unify error types. Consider using a more descriptive error message or using .ok() followed by .and_then() to avoid needing a context at all.

For example:

fn zip_mtime() -> DateTime {
    env::var("SOURCE_DATE_EPOCH")
        .ok()
        .and_then(|epoch| {
            let epoch: i64 = epoch.parse().ok()?;
            let dt = time::OffsetDateTime::from_unix_timestamp(epoch).ok()?;
            DateTime::try_from(dt).ok()
        })
        .unwrap_or_default()
}
Suggested change
let res = env::var("SOURCE_DATE_EPOCH")
.context("") // Only using context() to unify the error types
.and_then(|epoch| {
let epoch: i64 = epoch.parse()?;
let dt = time::OffsetDateTime::from_unix_timestamp(epoch)?;
let dt = DateTime::try_from(dt)?;
Ok(dt)
});
res.unwrap_or_default()
env::var("SOURCE_DATE_EPOCH")
.ok()
.and_then(|epoch| {
let epoch: i64 = epoch.parse().ok()?;
let dt = time::OffsetDateTime::from_unix_timestamp(epoch).ok()?;
DateTime::try_from(dt).ok()
})
.unwrap_or_default()

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not convinced that writing .ok()? everywhere is better than just ? but the code does the same thing either way so 🤷

@messense messense merged commit 8766f5f into PyO3:main Nov 21, 2025
51 checks passed
@e-nomem e-nomem deleted the wheel-file-options branch November 21, 2025 02:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants